package com.commonlib.downloadmgr.base;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Date;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Pair;

/**
 * Database builder template - facilitates adding and getting
 * objects from SQLite database
 * 
 * @param <T>
 */
public abstract class DatabaseBuilder<T> {
	private static final String TAG = DatabaseBuilder.class.getSimpleName();
	
	private Class< ? extends DownloadBaseJob<T> > clazzJob;
	private Class< T > clazzEntity;
	private String DB_NAME = "db_downloadmgr";
	private String TABLE_NAME = "table_jobs_entity";
	private DbHelper mHelper;
	private Context context;
	
	protected DatabaseBuilder(Context context, Class<? extends DownloadBaseJob<T>> clazzJob, Class<T> clazzEntity, String dbName, String tableName, int version){
		this.clazzJob = clazzJob;
		this.clazzEntity = clazzEntity;
		this.DB_NAME = dbName;
		this.TABLE_NAME = tableName;
		this.context = context;
		version = version < 1 ? 1 : version;
		mHelper = new DbHelper(context, DB_NAME, null, version);
	}
	
	public class DbHelper extends SQLiteOpenHelper{
		public DbHelper(Context context, String name, CursorFactory factory,
				int version) {
			super(context, name, factory, version);
		}

		@Override
		public void onCreate(SQLiteDatabase db) {
			create(db, true);
		}

		/**
		 * Initializes database and tables
		 */
		private void create(SQLiteDatabase db, boolean forceCreate){
			// create tables if necessary
			String conditionCreate = forceCreate ? "" : "IF NOT EXISTS ";
			StringBuilder sbSql = new StringBuilder();
			sbSql.append( "CREATE TABLE " )
				.append(conditionCreate)
		        .append( TABLE_NAME )
		        .append( "(_id INTEGER PRIMARY KEY AUTOINCREMENT " );
			ArrayList<Field> fields = new ArrayList<Field>();
			fields.addAll(getPersistFields(clazzJob));
			fields.addAll(getPersistFields(clazzEntity));
			for( Field field : fields ){
				sbSql.append( ',' )
					.append( field.getName() )
	            	.append( ' ' )
	            	.append( mappingType( field.getType() ) );
			}
			sbSql.append( ");" );
			Log.v(TAG, "create:" + sbSql.toString());
			db.execSQL(sbSql.toString());
		}
		
		
		@Override
		public void onOpen(SQLiteDatabase db) {
			Log.v(TAG, "onOpen");
			SQLiteDatabase tmp = db.isReadOnly() ? this.getWritableDatabase() : db;
			create( tmp, false );
		}

		@Override
		public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
			Log.v(TAG, "onUpgrade, oldVersion: " + oldVersion + ", newVersion: " + newVersion);
			db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
			onCreate(db);
		}
		
	}
	
	/**
	 * update or insert download job
	 * @param job
	 * @return insert: the row ID of the newly inserted row, or -1 if an error occurred
	 * 		   update: the number of rows affected
	 */
	public int saveDownloadJob(DownloadBaseJob<T> job){
		long retval = 0;
		SQLiteDatabase db = mHelper.getWritableDatabase();
		try {
			ContentValues values = deconstruct(job);
			String[] whereArgs = {job.getId()};
			retval = db.update(TABLE_NAME, values, "job_id = ?", whereArgs);
			Log.v(TAG, "update job, retval is " + retval);
			if (retval <= 0) {
				retval = db.insert(TABLE_NAME, null, values);
				Log.v(TAG, "insert job, retval is " + retval);
			}
			db.close();
		} catch (Exception e) {
			Log.w(TAG, "Exception saveDownloadJob: " + e.toString());
		}finally{
			if (db != null && db.isOpen()) {
				db.close();
				db = null;
			}
		}
		return (int) retval;
	}
	
	/**
	 * @param id
	 * @return the number of rows affected if a whereClause is passed in, 0 otherwise. To remove all rows and get a count pass "1" as the whereClause.
	 */
	public int deleteDownloadJob(String id){
		int numRows = 0;
		SQLiteDatabase db = mHelper.getWritableDatabase();
		try {
			String[] whereArgs = {id};
			numRows = db.delete(TABLE_NAME, "job_id = ?", whereArgs);
			Log.v(TAG, "delete job, retval is " + numRows);
		} catch (Exception e) {
			Log.w(TAG, "Exception deleteDownloadJob: " + e.toString());
		}finally{
			if (db != null && db.isOpen()) {
				db.close();
				db = null;
			}
		}
		return numRows;
	}
	
	public ArrayList<DownloadBaseJob<T>> getAllDownloadJobs(){
		ArrayList<DownloadBaseJob<T>> jobs = new ArrayList<DownloadBaseJob<T>>();
		SQLiteDatabase db = mHelper.getWritableDatabase();
		try {
			Cursor cursor = db.query(TABLE_NAME, null, null, null, null, null, null);
			
			while(cursor.moveToNext()){
				DownloadBaseJob<T> job = construct(cursor);
				if (job != null) {
					jobs.add(job);
				}
			}
		} catch (Exception e) {
			Log.w(TAG, "Exception getAllDownloadJobs: " + e.toString());
		}finally{
			if (db != null && db.isOpen()) {
				db.close();
				db = null;
			}
		}
		Log.v(TAG, "getAllDownloadJobs:" + jobs.toString());
		return jobs;
	}
	
	public DownloadBaseJob<T> getDownloadJob(String id){
		DownloadBaseJob<T> job = null;
		SQLiteDatabase db = mHelper.getWritableDatabase();
		try {
			Cursor cursor = db.query(TABLE_NAME, null, "job_id = ?", new String[]{id}, null, null, null);
			job = construct(cursor);
			Log.v(TAG, "getDownloadJob:" + job==null?"null":job.toString());
		} catch (Exception e) {
			Log.w(TAG, "Exception getDownloadJob: " + e.toString());
		}finally{
			if (db != null && db.isOpen()) {
				db.close();
				db = null;
			}
		}
		return job;
	}
	
	/**
	 * Creates object out of cursor
	 * 
	 * @param c
	 * @return
	 */
	@SuppressWarnings( "unchecked" )
	DownloadBaseJob<T> construct(Cursor cursor){
		DownloadBaseJob<T> job;
		job = (DownloadBaseJob<T>) generateObject(cursor, clazzJob);
		T entity = (T) generateObject(cursor, clazzEntity);
		job.setEntity(entity);
		job.setContext(context);
		job.setmHelper(getDownloadHelper());
		job.setmDownloadedSize(job.job_mDownloadedSize);
		return job;
	}
	
	/**
	 * Puts an object into a ContentValues instance
	 * 
	 * @param t
	 * @return
	 */
	ContentValues deconstruct(DownloadBaseJob<T> job){
		if (job == null) {
			return null;
		}
		ContentValues retVal = new ContentValues();
		retVal.putAll(getContentVaules(job));
		retVal.putAll(getContentVaules(job.getEntity()));
	    return retVal;
	}
	
	
	/**
	 * SQLite integer types��
	 */
	private static final String INTEGER = "INTEGER";
	/**
	 * SQLite string types��
	 */
	private static final String TEXT    = "TEXT";
	/**
	 * SQLite float types��
	 */
	private static final String REAL    = "REAL";
	  
	/**
	 * Used to mark a class or a class member can be SQLite stored
	 */
	@Target( { ElementType.TYPE, ElementType.FIELD } )
	@Retention( RetentionPolicy.RUNTIME )
	public @interface Persistable{
	}
	
	/**
	 * generate columns by sepcific class and annotation
	 * @param clazz
	 * @return
	 */
//	private ArrayList<String> generateColumns(Class<?> clazz){
//		ArrayList<String> arrayColumns = new ArrayList<String>();
//		ArrayList<Field> fields = getPersistFields(clazz);
//		for (Field field : fields) {
//			arrayColumns.add(field.getName());
//		}
//		return arrayColumns;
//	}
	
	/**
	   * For a given class of all need to store the public data members��
	   * @param clazz Class object of the specified class ��
	   * @return All the public data members need to be stored ��
	   */
	private ArrayList<Field> getPersistFields( Class< ? > clazz ){
	    ArrayList< Field > fields = new ArrayList< Field >();
	    Persistable persistable = clazz.getAnnotation( Persistable.class );
	    Field[] allFields = clazz.getFields();
	    for( Field field : allFields ){
	      int modifiers = field.getModifiers();
	      if( ! Modifier.isStatic( modifiers ) ){
	        if( persistable != null || field.isAnnotationPresent( Persistable.class ) ){
	          fields.add( field );
	        }
	      }
	    }
	    return fields;
	  }
	  
	  /**
	   * Mapping the Java data type into a SQLite data types��
	   * @param clazz Class object of the Specified type��
	   * @return Corresponding SQLite data types��
	   */
	private String mappingType( Class< ? > clazz ){
	    String retVal;
	    if( clazz.equals( Integer.TYPE )
	        || clazz.equals( Short.TYPE )
	        || clazz.equals( Long.TYPE )
	        || clazz.equals( Byte.TYPE )
	        || clazz.equals( Boolean.TYPE )
	        || clazz.equals( Integer.class )
	        || clazz.equals( Short.class )
	        || clazz.equals( Long.class )
	        || clazz.equals( Byte.class )
	        || clazz.equals( Date.class )
	        || clazz.equals( Boolean.class ) ){
	      retVal = INTEGER;
	    }else if( clazz.equals( Float.TYPE ) 
	        || clazz.equals( Double.TYPE )
	        || clazz.equals( Float.class )
	        || clazz.equals( Double.class ) ){
	      retVal = REAL;
	    }else{
	      retVal = TEXT;
	    }
	    return retVal;
	  }
	
	/**
	 *  get contentValues according to the persistFields.
	 * @param object
	 * @return
	 */
	private ContentValues getContentVaules(Object object){
		ContentValues retVal = new ContentValues();
		Class<?> rootClazz = object.getClass();
		ArrayList<Field> fields = getPersistFields(rootClazz );
		for( Field field : fields ){
			Class< ? > clazz = field.getType();
	        String fieldName = field.getName();
	        try{
	          if( clazz.equals( Byte.TYPE ) ){
	        	  retVal.put( fieldName, field.getByte( object ) );
	          }else if( clazz.equals( Integer.TYPE ) ){
	        	  retVal.put( fieldName, field.getInt( object ) );
	          }else if( clazz.equals( Float.TYPE ) ){
	        	  retVal.put( fieldName, field.getFloat( object ) );
	          }else if( clazz.equals( Short.TYPE ) ){
	        	  retVal.put( fieldName, field.getShort( object ) );
	          }else if( clazz.equals( Double.TYPE ) ){
	        	  retVal.put( fieldName, field.getDouble( object ) );
	          }else if( clazz.equals( Long.TYPE ) ){
	        	  retVal.put( fieldName, field.getLong( object ) );
	          }else if( clazz.equals( Boolean.TYPE ) ){
	        	  retVal.put( fieldName, field.getBoolean( object ) );
	          }else{
	        	  Object value = field.get( object );
	        	  if( clazz.equals( Integer.class ) ){
	        		  retVal.put( fieldName, value == null ? 0 : ( ( Integer )field.get( object ) ).intValue() );
	        	  }else if( clazz.equals( Byte.class ) ){
	        		  retVal.put( fieldName, value == null ? ( byte )0 : ( ( Byte )field.get( object ) ).byteValue() );
	        	  }else if( clazz.equals( Short.class ) ){
	        		  retVal.put( fieldName, value == null ? ( short )0 : ( ( Short )field.get( object ) ).shortValue() );
	        	  }else if( clazz.equals( Long.class ) ){
	        		  retVal.put( fieldName, value == null ? 0L : ( ( Long )field.get( object ) ).longValue() );
	        	  }else if( clazz.equals( Boolean.class ) ){
	        		  retVal.put( fieldName, value != null && ( Boolean )field.get( object ) ? true : false );
	        	  }else if( clazz.equals( Float.class ) ){
	        		  retVal.put( fieldName, value == null ? 0f : ( ( Float )field.get( object ) ).floatValue() );
	        	  }else if( clazz.equals( Double.class ) ){
	        		  retVal.put( fieldName, value == null ? 0.0 : ( ( Double )field.get( object ) ).doubleValue() );
	        	  }else if( clazz.equals( String.class ) ){
	        		  retVal.put( fieldName, value == null ? "" : ( String )field.get( object ) );
	        	  }else if( clazz.equals( Date.class ) ){
	        		  retVal.put( fieldName, value == null ? 0 : ( (Date)field.get( object ) ).getTime() );
	        	  }else{
	           		Log.e( TAG, "Invalid field type for " + fieldName + ": " + clazz.getName() );
	        	  }
          		}
	        }catch( IllegalAccessException ex ){
	          Log.e( TAG, "Can not access field " + field.getName(), ex );
	        }
	    }
		return retVal;
	}
	
	/**
	 * generate object according to persis persistFields
	 * @param cursor
	 * @param clazz
	 * @return
	 */
	private Object generateObject(Cursor cursor, Class<?> clazz){
		Object object = null;
         try{
           object = clazz.newInstance();
         }catch( IllegalAccessException ex ){
           Log.e( clazz.getName(), "Persist class must be public: " + clazz.getName(), ex );
         }catch( InstantiationException ex ){
           Log.e( clazz.getName(), "Instantiate class failed: " + clazz.getName(), ex );
         }
         int columnCount = cursor.getColumnCount();
         ArrayList<Pair<Field, Integer>> fields = new ArrayList<Pair<Field,Integer>>();
         for( int i = 0; i < columnCount; ++ i ){
           String columnName = cursor.getColumnName( i );
           Pair<Field, Integer> field = null;
           try{
             Field mfield = clazz.getField( columnName );
             field = new Pair<Field, Integer>(mfield, i);
           }catch( NoSuchFieldException ex ){
//             Log.w( TAG, "No field " + columnName + " in " +clazz.getName(), ex );
           }finally{
        	   if (field != null) {
				fields.add(field);
			}
           }
         }
         for(Pair<Field, Integer> field : fields){
        	 Class< ? > fieldClass = field.first.getType();
        	 try{
	             if( fieldClass.equals( Byte.TYPE ) || fieldClass.equals( Byte.class ) ){
	            	 field.first.setByte( object, ( byte )cursor.getInt( field.second ) );
	             }else if( fieldClass.equals( Integer.TYPE ) || fieldClass.equals( Integer.class ) ){
	            	 field.first.setInt( object, cursor.getInt( field.second ) );
	             }else if( fieldClass.equals( Float.TYPE ) || fieldClass.equals( Float.class ) ){
	            	 field.first.setFloat( object, cursor.getFloat(field.second) );
	             }else if( fieldClass.equals( Short.TYPE ) || fieldClass.equals( Short.class ) ){
	            	 field.first.setShort( object, cursor.getShort(field.second) );
	             }else if( fieldClass.equals( String.class ) ){
	            	 field.first.set( object, cursor.getString(field.second) );
	             }else if( fieldClass.equals( Double.TYPE ) || fieldClass.equals( Double.class ) ){
	            	 field.first.setDouble( object, cursor.getDouble(field.second) );
	             }else if( fieldClass.equals( Long.TYPE ) || fieldClass.equals( Long.class ) ){
	            	 field.first.setLong( object, cursor.getLong(field.second) );
	             }else if( fieldClass.equals( Boolean.TYPE ) || fieldClass.equals( Boolean.class ) ){
	            	 field.first.setBoolean( object, cursor.getInt(field.second) != 0 ? Boolean.TRUE : Boolean.FALSE );
	             }else if( fieldClass.equals( Date.class ) ){
	            	 field.first.set( object, new Date( cursor.getLong(field.second) ) );
	             }else{
	               Log.w( TAG, "Invalid field type for " + fieldClass.getName() + ": " + fieldClass.getName() );
	             }
	           }catch( IllegalAccessException ex ){
	             Log.w( TAG, "Can not access field " + fieldClass.getName() + ": " + fieldClass.getName(), ex );
	           }
         }
         return object;
	}
	
	protected abstract DownloadBaseHelper<T> getDownloadHelper();

}
